<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\DI\Config\Adapters;

use Nette;
use Nette\DI;
use Nette\DI\Definitions;
use Nette\DI\Definitions\Reference;
use Nette\DI\Definitions\Statement;
use Nette\Neon;
use Nette\Neon\Node;
use function array_walk_recursive, constant, count, defined, implode, is_array, is_string, ltrim, preg_match, preg_replace, sprintf, str_contains, str_ends_with, str_starts_with, substr;


/**
 * Reading and generating NEON files.
 */
final class NeonAdapter implements Nette\DI\Config\Adapter
{
	private const PreventMergingSuffix = '!';
	private string $file;
	private \WeakMap $parents;


	/**
	 * Reads configuration from NEON file.
	 */
	public function load(string $file): array
	{
		$input = Nette\Utils\FileSystem::read($file);
		if (substr($input, 0, 3) === "\u{FEFF}") { // BOM
			$input = substr($input, 3);
		}

		$this->file = $file;
		$decoder = new Neon\Decoder;
		$node = $decoder->parseToNode($input);
		$traverser = new Neon\Traverser;
		$node = $traverser->traverse($node, $this->deprecatedQuestionMarkVisitor(...));
		$node = $traverser->traverse($node, $this->removeUnderscoreVisitor(...));
		$node = $traverser->traverse($node, $this->convertAtSignVisitor(...));
		$node = $traverser->traverse($node, $this->deprecatedParametersVisitor(...));
		$node = $traverser->traverse($node, $this->resolveConstantsVisitor(...));
		$node = $traverser->traverse($node, $this->preventMergingVisitor(...));
		$this->connectParentsVisitor($traverser, $node);
		$node = $traverser->traverse($node, leave: $this->entityToExpressionVisitor(...));
		return (array) $node->toValue();
	}


	/** @deprecated */
	public function process(array $arr): array
	{
		return $arr;
	}


	/**
	 * Generates configuration in NEON format.
	 */
	public function dump(array $data): string
	{
		array_walk_recursive(
			$data,
			function (&$val): void {
				if ($val instanceof Statement) {
					$val = self::statementToEntity($val);
				}
			},
		);
		return "# generated by Nette\n\n" . Neon\Neon::encode($data, blockMode: true);
	}


	private static function statementToEntity(Statement $val): Neon\Entity
	{
		array_walk_recursive(
			$val->arguments,
			function (&$val): void {
				if ($val instanceof Statement) {
					$val = self::statementToEntity($val);
				} elseif ($val instanceof Reference) {
					$val = '@' . $val->getValue();
				}
			},
		);

		$entity = $val->getEntity();
		if ($entity instanceof Reference) {
			$entity = '@' . $entity->getValue();
		} elseif (is_array($entity)) {
			if ($entity[0] instanceof Statement) {
				return new Neon\Entity(
					Neon\Neon::Chain,
					[
						self::statementToEntity($entity[0]),
						new Neon\Entity('::' . $entity[1], $val->arguments),
					],
				);
			} elseif ($entity[0] instanceof Reference) {
				$entity = '@' . $entity[0]->getValue() . '::' . $entity[1];
			} elseif (is_string($entity[0])) {
				$entity = $entity[0] . '::' . $entity[1];
			}
		}

		return new Neon\Entity($entity, $val->arguments);
	}


	private function preventMergingVisitor(Node $node): void
	{
		if ($node instanceof Node\ArrayItemNode
			&& $node->key instanceof Node\LiteralNode
			&& is_string($node->key->value)
			&& str_ends_with($node->key->value, self::PreventMergingSuffix)
		) {
			if ($node->value instanceof Node\LiteralNode && $node->value->value === null) {
				$node->value = new Node\InlineArrayNode('[');
			} elseif (!$node->value instanceof Node\ArrayNode) {
				throw new Nette\DI\InvalidConfigurationException(sprintf(
					"Replacing operator is available only for arrays, item '%s' is not array (used in '%s')",
					$node->key->value,
					$this->file,
				));
			}

			$node->key->value = substr($node->key->value, 0, -1);
			$node->value->items[] = $item = new Node\ArrayItemNode;
			$item->key = new Node\LiteralNode(DI\Config\Helpers::PREVENT_MERGING);
			$item->value = new Node\LiteralNode(true);
		}
	}


	private function deprecatedQuestionMarkVisitor(Node $node): void
	{
		if ($node instanceof Node\EntityNode
			&& ($node->value instanceof Node\LiteralNode || $node->value instanceof Node\StringNode)
			&& is_string($node->value->value)
			&& str_contains($node->value->value, '?')
		) {
			throw new Nette\DI\InvalidConfigurationException("Operator ? is deprecated in config file (used in '$this->file')");
		}
	}


	private function entityToExpressionVisitor(Node $node): Node
	{
		if ($node instanceof Node\EntityChainNode) {
			return new Node\LiteralNode($this->buildExpression($node->chain));

		} elseif (
			$node instanceof Node\EntityNode
			&& !$this->parents[$node] instanceof Node\EntityChainNode
		) {
			return new Node\LiteralNode($this->buildExpression([$node]));

		} else {
			return $node;
		}
	}


	private function buildExpression(array $chain): Definitions\Expression
	{
		$node = array_pop($chain);
		$entity = $node->toValue();
		$stmt = new Statement(
			$chain ? [$this->buildExpression($chain), ltrim($entity->value, ':')] : $entity->value,
			$entity->attributes,
		);

		if ($this->isFirstClassCallable($node)) {
			$entity = $stmt->getEntity();
			if (is_array($entity)) {
				if ($entity[0] === '') {
					return new Definitions\FunctionCallable($entity[1]);
				}
				return new Definitions\MethodCallable(...$entity);
			} else {
				throw new Nette\DI\InvalidConfigurationException("Cannot create closure for '$entity' in config file (used in '$this->file')");
			}
		}

		return $stmt;
	}


	private function isFirstClassCallable(Node\EntityNode $node): bool
	{
		return array_keys($node->attributes) === [0]
			&& $node->attributes[0]->key === null
			&& $node->attributes[0]->value instanceof Node\LiteralNode
			&& $node->attributes[0]->value->value === '...';
	}


	private function removeUnderscoreVisitor(Node $node): void
	{
		if (!$node instanceof Node\EntityNode) {
			return;
		}

		$index = false;
		foreach ($node->attributes as $i => $attr) {
			if ($attr->key !== null) {
				continue;
			}

			$attr->key = $index ? new Node\LiteralNode((string) $i) : null;

			if ($attr->value instanceof Node\LiteralNode && $attr->value->value === '_') {
				unset($node->attributes[$i]);
				$index = true;
			}
		}
	}


	private function convertAtSignVisitor(Node $node): void
	{
		if ($node instanceof Node\StringNode) {
			if (str_starts_with($node->value, '@@')) {
				trigger_error("There is no need to escape @ anymore, replace @@ with @ in: '$node->value' (used in $this->file)", E_USER_DEPRECATED);
			} else {
				$node->value = preg_replace('#^@#', '$0$0', $node->value); // escape
			}

		} elseif (
			$node instanceof Node\LiteralNode
			&& is_string($node->value)
			&& str_starts_with($node->value, '@@')
		) {
			trigger_error("There is no need to escape @ anymore, replace @@ with @ and put string in quotes: '$node->value' (used in $this->file)", E_USER_DEPRECATED);
		}
	}


	private function deprecatedParametersVisitor(Node $node): void
	{
		if (($node instanceof Node\StringNode || $node instanceof Node\LiteralNode)
			&& is_string($node->value)
			&& str_contains($node->value, '%parameters%')
		) {
			trigger_error('%parameters% is deprecated, use @container::getParameters() (in ' . $this->file . ')', E_USER_DEPRECATED);
		}
	}


	private function resolveConstantsVisitor(Node $node): void
	{
		$items = match (true) {
			$node instanceof Node\ArrayNode => $node->items,
			$node instanceof Node\EntityNode => $node->attributes,
			default => null,
		};
		if ($items) {
			foreach ($items as $item) {
				if ($item->value instanceof Node\LiteralNode
					&& is_string($item->value->value)
					&& preg_match('#^([\w\\\]*)::[A-Z]\w+$#D', $item->value->value)
					&& defined(ltrim($item->value->value, ':'))
				) {
					$item->value->value = constant(ltrim($item->value->value, ':'));
				}
			}
		}
	}


	private function connectParentsVisitor(Neon\Traverser $traverser, Node $node): void
	{
		$this->parents = new \WeakMap;
		$stack = [];
		$traverser->traverse(
			$node,
			enter: function (Node $node) use (&$stack) {
				$this->parents[$node] = end($stack);
				$stack[] = $node;
			},
			leave: function () use (&$stack) {
				array_pop($stack);
			},
		);
	}
}
